/*
 * Copyright 2011 yingxinwu.g@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package xink.vpn;

import static xink.vpn.Constants.*;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Timer;
import java.util.TimerTask;

import xink.vpn.wrapper.VpnProfile;
import xink.vpn.wrapper.VpnState;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

/**
 * Keep VPN connection alive, by accessing internet resource periodically.
 */
public final class KeepAlive extends BroadcastReceiver {

	public static final String PREF_HEARTBEAT_PERIOD = "xink.vpn.pref.keepAlive.period";

	public static final String PREF_ENABLED = "xink.vpn.pref.keepAlive";

	private static final String TAG = KeepAlive.class.getName();

	private static Timer timer = new Timer("xink.vpn.HeartbeatTimer", true);

	private static Heartbeat heartbeat;

	/**
	 * Get informed about VPN connectivity, to start/stop the heartbeat session.
	 */
	@Override
	public void onReceive(final Context context, final Intent intent) {
		String action = intent.getAction();
		if (!ACTION_VPN_CONNECTIVITY.equals(action))
			return;

		VpnProfileRepository repo = VpnProfileRepository.getInstance(context);
		VpnProfile p = repo.getActiveProfile();

		String profileName = intent.getStringExtra(BROADCAST_PROFILE_NAME);
		if (p == null || profileName == null
				|| !profileName.equals(p.getName())) {
			// Log.d(TAG, "ignores non-active profile event: " + profileName);
			return;
		}

		SharedPreferences prefs = PreferenceManager
				.getDefaultSharedPreferences(context);

		VpnState newState = Utils.extractVpnState(intent);
		stateChanged(p, newState, prefs);
	}

	private void stateChanged(final VpnProfile p, final VpnState newState,
			final SharedPreferences prefs) {
		// Log.d(TAG, p + " state ==> " + newState);

		switch (newState) {
		case CONNECTED:
			startHeartbeat(p, prefs);
			break;
		case IDLE:
			stopHeartbeat();
			break;
		default:
			break;
		}
	}

	private static void startHeartbeat(final VpnProfile p,
			final SharedPreferences prefs) {
		boolean enabled = prefs.getBoolean(PREF_ENABLED, false);
		if (!enabled || heartbeat != null)
			return;

		int period = getPeriodFromPrefs(prefs);
		Log.d(TAG, "start heartbeat every (ms)" + period);

		heartbeat = new Heartbeat(p);
		timer.schedule(heartbeat, period, period);
	}

	private static int getPeriodFromPrefs(final SharedPreferences prefs) {
		String periodStr = prefs.getString(PREF_HEARTBEAT_PERIOD,
				Period.TEN_MIN.toString());
		Period p = Period.valueOf(periodStr);
		return p.value;
	}

	private static synchronized void stopHeartbeat() {
		if (heartbeat == null)
			return;

		heartbeat.cancel();
		int removed = timer.purge();
		heartbeat = null;
		Log.d(TAG, "removed heartbeat timerTask: " + removed);
	}

	private static class Heartbeat extends TimerTask {

		private VpnProfile profile; // current hearbeat vpn profile

		protected Heartbeat(final VpnProfile p) {
			this.profile = p;
		}

		@Override
		public void run() {
			try {
				execPing();
			} catch (IOException e) {
				Log.e(TAG, "heartdbeat error", e);
			}
		}

		// ping the vpn server to keep connection alive
		private void execPing() throws IOException {
			Process process = null;

			try {
				process = new ProcessBuilder("sh").redirectErrorStream(true)
						.start();

				DataOutputStream os = new DataOutputStream(
						process.getOutputStream());
				os.writeBytes("ping -c 10 " + profile.getServerName() + "\n");
				os.writeBytes("exit\n");
				os.flush();

				dumpPingResults(process);

			} finally {
				if (process != null) {
					process.destroy();
				}
			}
		}

		private void dumpPingResults(final Process process) throws IOException {
			BufferedReader reader = new BufferedReader(new InputStreamReader(
					process.getInputStream()));
			String line;

			while ((line = reader.readLine()) != null) {
				Log.d(TAG, line);
			}
		}
	}

	private static enum Period {
		FIVE_MIN(300000), TEN_MIN(600000), FIFTEEN_MIN(900000), THIRTY_MIN(
				1800000), TEST_5_SEC(5000);

		private int value; // in miliseconds

		private Period(final int v) {
			value = v;
		}
	}
}
